// $Id: dispatcher.c 86 2011-05-04 17:33:43Z cedric.shih@gmail.com $
/*
 * Copyright (C) 2011 Cedric Shih <cedric.shih@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#include <nio/handler.h>
#include <nio/buffer.h>
#include <niohttp.h>
#include <niohttp/server.h>

#include "log.h"

#include "stream.h"
#include "handler.h"
#include "helper.h"
//#include "multipart.h"

#include "dispatcher.h"

struct niohandler niohttpd_dispatcher = {
	niohttpd_dispatcher_setup,
	niohttpd_dispatcher_teardown,
	niohttpd_dispatcher_handle_upstream,
	niohttpd_dispatcher_handle_downstream,
	niohttpd_dispatcher_free,
};

struct niohttpd_dispatcher *niohttpd_dispatcher_new(struct niohttpd *server)
{
	struct niohttpd_dispatcher *dispatcher;

	niohttpd_debug("constructing");

	if (!(dispatcher = calloc(1, sizeof(*dispatcher)))) {
		niohttpd_error("failed to calloc");
		goto exception;
	}

	if (!(dispatcher->decoder = niohttp_request_decoder_new())) {
		niohttpd_error("failed to create request decoder");
		goto exception;
	}

	if (!(dispatcher->encoder = niohttp_response_encoder_new())) {
		niohttpd_error("failed to create response encoder");
		goto exception;
	}

	dispatcher->server = server;
	return dispatcher;

exception:
	niohttpd_dispatcher_free(dispatcher);
	return NULL;
}

void niohttpd_dispatcher_free(void *obj)
{
	struct niohttpd_dispatcher *dispatcher = obj;

	niohttpd_debug("destructing: %p", dispatcher);

	if (!dispatcher) {
		return;
	}

	if (dispatcher->encoder) {
		niohttp_response_encoder_free(dispatcher->encoder);
	}

	if (dispatcher->decoder) {
		niohttp_request_decoder_free(dispatcher->decoder);
	}

	free(dispatcher);
}

int niohttpd_dispatcher_setup(void *obj, const char *name)
{
	int rc;
	struct niohttpd_dispatcher *dispatcher = obj;

	if (!dispatcher || !name) {
		niohttpd_error("null argument(s)");
		rc = EINVAL;
		goto finally;
	}

	niohttpd_debug("setting up: %s", name);

	if ((rc = niohttp_request_decoder_setup(dispatcher->decoder))) {
		niohttpd_error("failed to setup decoder: %s",
				niohttp_strerror(rc));
		goto finally;
	}

	if ((rc = niohttp_response_encoder_setup(dispatcher->encoder))) {
		niohttpd_error("failed to setup encoder: %s",
				niohttp_strerror(rc));
		goto finally;
	}

	dispatcher->name = name;
	rc = 0;

finally:
	return rc;
}

int niohttpd_dispatcher_teardown(void *obj)
{
	int rc;
	struct niohttpd_dispatcher *dispatcher = obj;

	if (!dispatcher) {
		niohttpd_error("null argument(s)");
		rc = EINVAL;
		goto finally;
	}

	niohttpd_debug("tearing down: %s", dispatcher->name);

	if (dispatcher->context.response) {
		niohttp_response_free(dispatcher->context.response);
		dispatcher->context.response = NULL;
	}

	dispatcher->name = NULL;
	rc = 0;

finally:
	return rc;
}

static int niohttpd_dispatcher_process(struct niohttpd_dispatcher *dispatcher,
		struct niochannel *channel,
		struct niohttp_request *request)
{
	int rc, major, minor, keep_alive;
	const char *method, *clen,*conn;

	method = niohttp_request_method(request);
	major = niohttp_request_major_version(request);
	minor = niohttp_request_minor_version(request);
	keep_alive = 0;

	if (major == 1) {
		conn = niohttp_request_header(request, "Connection");

		if (minor == 0) {
			if (conn && !strcasecmp(conn, "Keep-Alive")) {
				keep_alive = 1;
			}
		} else if (minor == 1) {
			if (!conn || strcasecmp(conn, "Close")) {
				keep_alive = 1;
			}
		}
	}

	if (keep_alive && (rc = niochannel_keep_alive(
			channel))) {
		niohttpd_error("failed to keep channel alive: %s",
				nio_strerror(rc));
		goto finally;
	}

	rc = 0;

finally:
	return rc;
}

int niohttpd_dispatcher_handle_upstream(void *obj,
		struct niochannel *channel, void *message)
{
	int rc;
	struct niohttpd_dispatcher *dispatcher = obj;
	struct niohttp_request *req;
	struct niohttp_response *resp;
	struct niobuffer *buffer;
	struct niohttpd_handler_entry *handler;
	const char *header;
	char *ptr;
	long clen = 0;

	if (!dispatcher || !message || !channel) {
		niohttpd_error("null pointer");
		rc = EINVAL;
		goto finally;
	}

	niohttpd_debug("handling upstream: %s", dispatcher->name);

	if (!dispatcher->handler) {
		buffer = message;

		if ((rc = niohttp_request_decoder_parse(dispatcher->decoder,
				buffer, &req))) {
			niohttpd_error("failed to parse request: %s",
					strerror(rc));
			goto finally;
		}

		if (!req) { // read more
			rc = 0;
			goto finally;
		}

		if (niohttpd_request_body_expected(
				niohttp_request_method(req))) {
			if (!(header = niohttp_request_header(
					req, "Content-Length"))) {
				niohttpd_error("no content-length but body was "
						"expected in method: %s",
						niohttp_request_method(req));
				rc = NIOHTTPD_EINVAL;
				goto finally;
			}

			clen = strtol(header, &ptr, 10);

			if (ptr != header + strlen(header) || clen < 0) {
				niohttpd_error("invalid content-length: %s",
						header);
				rc = NIOHTTPD_EINVAL;
				goto finally;
			}
		}

		if ((handler = niohttpd_match_handler(dispatcher->server,
				niohttp_request_uri(req)))) {
			niohttpd_debug("invoking %p to create handler: %s",
					handler->factory.create, handler->uri);

			if (!(dispatcher->handler = (*handler->factory.create)(
					handler->cbarg))) {
				niohttpd_error("failed to create handler: %s",
						strerror(errno));
				rc = NIOHTTPD_EUNKNOWN;
				goto finally;
			}
		} else {
			niohttpd_warn("no matching handler: %s",
					niohttp_request_uri(req));

			dispatcher->handler = &niohttpd_404_handler;
		}

		if (!(resp = niohttp_request_create_response(
				req, 200, "OK"))) {
			niohttpd_error("failed to create response");
			rc = NIOHTTPD_ENOMEM;
			goto finally;
		}

		dispatcher->context.dispatcher = dispatcher;
		dispatcher->context.request = req;
		dispatcher->context.response = resp;
		dispatcher->future.context = &dispatcher->context;
		dispatcher->future.readable.remaining = clen;

		niohttpd_debug("invoking %p to handle request: %s",
				dispatcher->handler->handle, handler->uri);

		if ((rc = (*dispatcher->handler->handle)(&dispatcher->context,
				&dispatcher->future, handler->cbarg))) {
			niohttpd_error("failed to handle request: %s",
					strerror(rc));
			rc = NIOHTTPD_EUNKNOWN;
			goto finally;
		}
	}

	rc = 0;

finally:
	if (rc != 0) {
		if (dispatcher->context.response) {
			niohttp_response_free(dispatcher->context.response);
			dispatcher->context.response = NULL;
		}

//		if (dispatcher->tunnel.dispatcher) {
//			niohttpd_debug("invoking %p to free tunnel dispatcher",
//					dispatcher->tunnel.ops->free);
//			(*dispatcher->tunnel.ops->free)(dispatcher->tunnel.dispatcher);
//			dispatcher->tunnel.dispatcher = NULL;
//		}
	}
	return rc;
}

int niohttpd_dispatcher_handle_not_found(struct niohttpd_dispatcher *dispatcher,
		struct niochannel *channel, struct niohttp_request *req)
{
	int rc;
	struct niobuffer *content;
	char temp[11];

	content = niochannel_buffer(channel);
	niobuffer_clear(content);

	if ((rc = niobuffer_putf(content, "<html>"
			"<head>"
			"<title>404 Not Found</title>"
			"</head>"
			"<body>"
			"<h1>Not Found</h1>"
			"<p>"
			"The requested URL %s was not found on this "
			"server."
			"</p>"
			"</body>"
			"</html>",
			niohttp_request_uri(req)))) {
		niohttpd_error("failed to putf: %s", nio_strerror(rc));
		goto finally;
	}

	niobuffer_flip(content);

	if (!(dispatcher->context.response = niohttp_request_create_response(
			req, 404, "Not Found"))) {
		niohttpd_error("failed to create response");
		rc = ENOMEM;
		goto finally;
	}

	if ((rc = niohttp_response_set_header(dispatcher->context.response,
			"Content-Type", "text/html"))) {
		niohttpd_error("failed to set content-type: %s",
				niohttpd_strerror(rc));
		goto finally;
	}

	snprintf(temp, sizeof(temp), "%d", niobuffer_remaining(content));

	if ((rc = niohttp_response_set_header(dispatcher->context.response,
			"Content-Length", temp))) {
		fprintf(stdout, "failed to set content-length: "
				"%s\n", niohttpd_strerror(rc));
		goto finally;
	}

	if (!(dispatcher->header = niobuffer_slice(content,
			niobuffer_limit(content),
			niobuffer_capacity(content) -
			niobuffer_limit(content)))) {
		niohttpd_error("failed to slice header buffer");
		rc = -1;
		goto finally;
	}

	if ((rc = niohttp_response_encoder_format(dispatcher->encoder,
			dispatcher->context.response, dispatcher->header))) {
		niohttpd_error("failed to format response: %s",
				niohttpd_strerror(rc));
		goto finally;
	}

	niobuffer_flip(dispatcher->header);

	if (!(dispatcher->wrapper = niobuffer_wrapper())) {
		niohttpd_error("failed to create wrapper");
		rc = NIOHTTPD_ENOMEM;
		goto finally;
	}

	if ((rc = niobuffer_wrap(dispatcher->wrapper, dispatcher->header))) {
		niohttpd_error("failed to wrap header: %s", nio_strerror(rc));
		goto finally;
	}

	if ((rc = niobuffer_wrap(dispatcher->wrapper, content))) {
		niohttpd_error("failed to wrap content: %s", nio_strerror(rc));
		goto finally;
	}

	if ((rc = niochannel_send_downstream(channel,
			dispatcher->name, dispatcher->wrapper))) {
		niohttpd_error("failed to send downstream: %s",
				nio_strerror(rc));
		rc = NIOHTTPD_EIO;
		goto finally;
	}

finally:
	return rc;
}

void niohttpd_dispatcher_on_channel_closable(struct niochannel *channel,
		void *arg)
{
	int rc;
	struct niohttpd_dispatcher *dispatcher = arg;

	if ((rc = niochannel_close(channel))) {
		niohttpd_error("failed to close channel: %s", nio_strerror(rc));
		goto finally;
	}

finally:
	return;
}

int niohttpd_dispatcher_handle_downstream(void *obj,
		struct niochannel *channel, void *message)
{
	int rc;
	struct niohttpd_dispatcher *dispatcher = obj;

	if (!dispatcher) {
		niohttpd_error("null pointer");
		rc = EINVAL;
		goto finally;
	}

	niohttpd_debug("handling downstream: %s", dispatcher->name);
//
//	if (dispatcher->tunnel.dispatcher) {
//		niohttpd_debug("invoking %p to tunnel downstream message",
//				dispatcher->tunnel.ops->handle_downstream);
//
//		if ((rc = (*dispatcher->tunnel.ops->handle_downstream)(
//				dispatcher->tunnel.dispatcher, channel, message))) {
//			niohttpd_error("failed to handle downstream message: "
//					"%s", strerror(rc));
//			goto finally;
//		}
//
//		rc = 0;
//		goto finally;
//	}

	if ((rc = niochannel_send_downstream(channel, dispatcher->name,
			message))) {
		niohttpd_error("failed to send downstream: %s",
				nio_strerror(rc));
		goto finally;
	}

finally:
	return rc;
}

int niohttpd_404_handler_on_request(struct niohttpd_context *context,
		struct niohttpd_future *future, void *arg)
{
	int rc;

	if ((rc = niohttpd_future_replyable(future,
			niohttpd_404_handler_on_replyable, arg))) {
		niohttpd_error("failed to listen replyable: %s",
				niohttp_strerror(rc));
		goto finally;
	}

	rc = 0;

finally:
	return rc;
}

int niohttpd_404_handler_on_replyable(struct niohttpd_context *context,
		struct niohttpd_future *future, void *arg)
{
	int rc;
	struct niohttp_request *req = niohttpd_context_request(context);
	struct niohttp_response *resp = niohttpd_context_response(context);
	struct niobuffer *buffer = niohttpd_context_buffer(context);

	if ((rc = niohttp_response_set_status(resp, 404, "Not Found"))) {
		niohttpd_error("failed to set status: %s",
				niohttp_strerror(rc));
		goto finally;
	}

	if ((rc = niohttp_response_set_header(resp,
			"Content-Type", "text/html"))) {
		niohttpd_error("failed to set content-type: %s",
				niohttpd_strerror(rc));
		goto finally;
	}

	niobuffer_clear(buffer);

	if ((rc = niobuffer_putf(buffer, "<html>"
			"<head>"
			"<title>404 Not Found</title>"
			"</head>"
			"<body>"
			"<h1>Not Found</h1>"
			"<p>"
			"The requested URL %s was not found on this "
			"server."
			"</p>"
			"</body>"
			"</html>",
			niohttp_request_uri(req)))) {
		niohttpd_error("failed to putf: %s", nio_strerror(rc));
		goto finally;
	}

	niobuffer_flip(buffer);

	rc = 0;

finally:
	return rc;
}
